Géolocalisation de données textuelles

Author

Pascal Brissette

Introduction

Produire une carte à partir d’un jeu de données trouvé en ligne est désormais une opération assez simple. Quelques lignes de code peuvent peuvent suffire, comme le montre l’exemple ci-dessous mettant à profit un jeu de données sur les crimes commis sur le territoire montréalais (l’importation du jeu de données peut prendre plusieurs secondes, selon la vitesse de votre connexion internet) :

if(!"geojsonsf" %in% rownames(installed.packages())) {install.packages("geojsonsf")}
if(!"dplyr" %in% rownames(installed.packages())) {install.packages("dplyr")}
if(!"mapview" %in% rownames(installed.packages())) {install.packages("mapview")}

library(geojsonsf)
library(dplyr)

Attaching package: 'dplyr'
The following objects are masked from 'package:stats':

    filter, lag
The following objects are masked from 'package:base':

    intersect, setdiff, setequal, union
library(mapview)

# Importation d'un fichier comportant les coordonnées géographiques de crimes commis à Montréal
# Élimination à la volée des données non pourvues de coordonnées, puis sélection aléatoire d'un échantillon de 500 observations
geo <- geojson_sf("https://data.montreal.ca/dataset/5829b5b0-ea6f-476f-be94-bc2b8797769a/resource/aacc4576-97b3-4d8d-883d-22bbca41dbe6/download/actes-criminels.geojson") |>
  filter(!is.na(LONGITUDE)) |> 
  slice_sample(n=500)

# Création de la carte avec le jeu de données importé
mapview(geo["CATEGORIE"])

On arrive, avec très peu de moyens, à produire grâce à l’extension mapview une carte interactive d’une très grande qualité.

Il faut dire que R possède des extensions de grande qualité développées, soutenues et utilisées par les géographes. Si vous possédez un jeu de données qui comporte les coordonnées géographiques d’objets du monde réel et que ce jeu répond aux standards Simple Feature Access (SF), le passage vers la représentation cartographique sera pratiquement un jeu d’enfant. Vous trouverez en ligne quantité de cours et de tutoriels qui vous montreront à utiliser les fonctions de l’une ou l’autre des extentions spécialisées.

L’objectif de l’atelier n’est pas d’ajouter à cette documentation abondante. Plutôt, il vise à faire le pont entre l’analyse des données textuelles et les extensions spécialisées. Même si l’exercice débouchera, deux fois plutôt qu’une, sur la création de cartes géographiques, on veut prendre le problème en amont de l’exercice cartographique en proposant une chaîne de traitement possible pour passer des données textuelles aux cartes géographiques.

Objectifs:

  • repérer dans des textes des objets du monde réel qui puissent être géolocalisés (sous forme de polygones et sous forme de points), puis extraire les chaînes de caractères représentant ces objets;

  • obtenir par croisement de tables et par API (tidygeocoder) les coordonnées géographiques de ces objets;

  • projeter les résultats sur des carte.

Nous allons essayer d’atteindre ces objectifs à travers deux tâches plus précises.

Première tâche

La première tâche consistera à extraire de textes de fictions les noms de quartiers de Montréal et à projeter ensuite sur une carte géographique ces mêmes quartiers sous forme de polygones. Une échelle de couleurs sera établie pour traduire la fréquence d’apparition des noms de quartiers dans l’ensemble du corpus.

Deuxième tâche

La deuxième tâche consistera à extraire du même corpus les mentions des universités montréalaises, à géoréférencer ces universitiés avec tidygeocoder, puis à projeter sur une carte géographique les points correspondant à ces objets.

Installation et activation des extensions

Il convient d’abord de préparer l’environnement de travail. La fonction ci-dessous permet d’importer dans l’environnement des extensions si elles ne le sont pas déjà, puis de les activer.

#| echo: false

# Création d'une fonction d'installation et d'activation des extensions
inst_ext_fun <- function(extension) {
  if(!extension %in% rownames(installed.packages())) {
    install.packages(extension, dependencies = TRUE)
    }
  require(extension, character.only = TRUE)
  }

# usage
extensions <-
  c(
    "ggplot2",
    "leaflet",
    "leaflet.extras",
    "data.table",
    "tidygeocoder",
    "sf",
    "dplyr",
    "stringi",
    "stringr",
    "tmap",
    "tmaptools",
    "viridis",
    "geojsonsf",
    "mapview"
  )

# Application de la fonction à chaque élément du vecteur `extensions`
sapply(extensions, inst_ext_fun)
Loading required package: ggplot2
Loading required package: leaflet
Loading required package: leaflet.extras
Loading required package: data.table

Attaching package: 'data.table'
The following objects are masked from 'package:dplyr':

    between, first, last
Loading required package: tidygeocoder
Loading required package: sf
Linking to GEOS 3.11.0, GDAL 3.5.3, PROJ 9.1.0; sf_use_s2() is TRUE
Loading required package: stringi
Loading required package: stringr
Loading required package: tmap
Loading required package: tmaptools
Loading required package: viridis
Loading required package: viridisLite
       ggplot2        leaflet leaflet.extras     data.table   tidygeocoder 
          TRUE           TRUE           TRUE           TRUE           TRUE 
            sf          dplyr        stringi        stringr           tmap 
          TRUE           TRUE           TRUE           TRUE           TRUE 
     tmaptools        viridis      geojsonsf        mapview 
          TRUE           TRUE           TRUE           TRUE 

Les données textuelles

Importation et préparation des données

Le jeu de données que nous allons utiliser est celui qui a été exploré dans les précédents ateliers. Il s’agit d’un tableau de données (data frame) contenant 563 nouvelles littéraires publiées dans XYZ: la revue de la nouvelle entre 2012 et 2022. Les textes ont été moissonnés sur le site Érudit par Amélie Ducharme et Yu Chen Shi, sous la supervision de Julien Vallières-Gingras.

# Lecture du jeu de données
xyz <-
  data.table::fread("donnees/donnees_importees/xyz.csv")

# Observer la structure
str(xyz)
Classes 'data.table' and 'data.frame':  563 obs. of  7 variables:
 $ Titre : chr  "Le labyrinthe" "Échoueries" "Old Harry" "Atshen" ...
 $ Auteur: chr  "Louise Dupré" "Claire Legendre" "Marie-Andrée Arsenault" "Charles Sagalane" ...
 $ Numéro: chr  "149" "149" "149" "149" ...
 $ Date  : chr  "printemps 2022" "printemps 2022" "printemps 2022" "printemps 2022" ...
 $ Thème : chr  "Îles : l’archipel des solitudes" "Îles : l’archipel des solitudes" "Îles : l’archipel des solitudes" "Îles : l’archipel des solitudes" ...
 $ URI   : chr  "https://id.erudit.org/iderudit/97693ac" "https://id.erudit.org/iderudit/97694ac" "https://id.erudit.org/iderudit/97695ac" "https://id.erudit.org/iderudit/97696ac" ...
 $ Texte : chr  "Le traversier accosterait dans vingt minutes environ, une femme venait de le mentionner en anglais à une autre "| __truncated__ "Ça aurait été l’histoire d’une écrivaine triste partie dépenser ce qu’il lui restait d’argent dans l’archipel d"| __truncated__ "Ses mains portaient les entailles profondes que font les filins au bout desquels se débattent les lourds poisso"| __truncated__ "Supposez que nous soyons à cinquante mille lunes d’ici. Je ne parle pas de distance, mais bien de l’astre qui a"| __truncated__ ...
 - attr(*, ".internal.selfref")=<externalptr> 
# Il n'y a pas d'identifiant unique. On peut en composer en utilisant, dans les URL moissonnés, les séquences numériques uniques.
xyz$doc_id <-
  as.integer(
    str_sub(
      xyz$URI, start = -7L, end = -3L
      )
    )

# Vérifier que les doc_id sont uniques
table(!duplicated(xyz$doc_id))

TRUE 
 563 
# Observer à nouveau la structure
str(xyz)
Classes 'data.table' and 'data.frame':  563 obs. of  8 variables:
 $ Titre : chr  "Le labyrinthe" "Échoueries" "Old Harry" "Atshen" ...
 $ Auteur: chr  "Louise Dupré" "Claire Legendre" "Marie-Andrée Arsenault" "Charles Sagalane" ...
 $ Numéro: chr  "149" "149" "149" "149" ...
 $ Date  : chr  "printemps 2022" "printemps 2022" "printemps 2022" "printemps 2022" ...
 $ Thème : chr  "Îles : l’archipel des solitudes" "Îles : l’archipel des solitudes" "Îles : l’archipel des solitudes" "Îles : l’archipel des solitudes" ...
 $ URI   : chr  "https://id.erudit.org/iderudit/97693ac" "https://id.erudit.org/iderudit/97694ac" "https://id.erudit.org/iderudit/97695ac" "https://id.erudit.org/iderudit/97696ac" ...
 $ Texte : chr  "Le traversier accosterait dans vingt minutes environ, une femme venait de le mentionner en anglais à une autre "| __truncated__ "Ça aurait été l’histoire d’une écrivaine triste partie dépenser ce qu’il lui restait d’argent dans l’archipel d"| __truncated__ "Ses mains portaient les entailles profondes que font les filins au bout desquels se débattent les lourds poisso"| __truncated__ "Supposez que nous soyons à cinquante mille lunes d’ici. Je ne parle pas de distance, mais bien de l’astre qui a"| __truncated__ ...
 $ doc_id: int  97693 97694 97695 97696 97697 97698 97699 97700 97701 97703 ...
 - attr(*, ".internal.selfref")=<externalptr> 
# La variable "Date" contient l'année. On peut extraire cette information et l'emmagasiner dans une nouvelle colonne
xyz$annee <- as.integer(str_extract(string = xyz$Date, pattern = "[0-9]+"))

## Exercice: créez une table montrant la distribution des numéros par année
# table()

# Faire le même exercice avec la variable `numéro`. Que remarquez-vous?
table(xyz$Numéro)

       109        110        111        112        133        134        135 
        13         11         22          9         16          9         12 
       136        138        139        140        141        142        143 
        16         18         14         13         12         14         14 
       144        149        150        151        152 Numéro 113 Numéro 114 
        13         12         20         13         10         12          7 
Numéro 115 Numéro 116 Numéro 121 Numéro 122 Numéro 123 Numéro 124 Numéro 125 
        15         55         12         22         13         11          9 
Numéro 126 Numéro 127 Numéro 128 Numéro 129 Numéro 130 Numéro 131 Numéro 132 
        15         13         10         14         16         14         12 
Numéro 145 Numéro 146 Numéro 147 Numéro 148 
        12         14         13         13 
# Il faut d'abord extraire les nombres (irrégularité dans l'entrée de données)
# L'argument `pattern` est une expression régulière. Pourriez-vous l'expliquer?
xyz$Numéro <- as.integer(str_extract(xyz$Numéro, pattern = "[0-9]{1,3}"))

# Essayer à nouveau
table(xyz$Numéro)

109 110 111 112 113 114 115 116 121 122 123 124 125 126 127 128 129 130 131 132 
 13  11  22   9  12   7  15  55  12  22  13  11   9  15  13  10  14  16  14  12 
133 134 135 136 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 
 16   9  12  16  18  14  13  12  14  14  13  12  14  13  13  12  20  13  10 
# Hourra!

# Donner des noms adéquats aux colonnes
colnames(xyz) <-
  c("titre",
    "auteur",
    "numero",
    "Date",
    "theme",
    "url",
    "texte",
    "doc_id", 
    "annee")


# Allègement de la structure de données
xyz <- xyz[, c("doc_id", "titre", "auteur", "numero", "annee", "theme", "texte")]

# str(xyz)

# Exporter cette table
# fwrite(xyz, "donnees/donnees_produites/csv/combined_table_net_xyz.csv")

Première tâche

Les quartiers montréalais comme objets géographiques

Les données géographiques

Pour que des lieux physiques puissent être transposés sur une carte géographique, il faut en proposer une représentation qui respecte un ensemble de normes. La norme Simple Feature Access est celle que les spécialistes suivent dans la construction, le stockage et le traitement des objets pourvus d’attributs géographiques.

Dans R, l’extension sf implémente cette norme Simple Feature Access (ou simplement SF). Ce qu’on appelle feature peut être conçu comme un objet du monde réel: un immeuble, une place publique, une ville, un cours d’eau, une forêt, une chaîne de montagnes. Une ville par exemple, selon la perspective (le point dans l’espace depuis lequel on la considère), sera représentée par un point, un polygone ou un ensemble de polygones (si elle est traversée par des cours d’eau). Une rivière prendra généralement la forme d’une série de points formant une ligne brisée, et ainsi de suite. Les points, les polygones, les multipolygones et les lignes sont les principales formes avec lesquels on représente les objets sur une carte. Un objet géoréférencé aura d’autres attributs (exemple: population, température moyenne, altitude, etc.). Toutes ces données, géographiques et autres, seront emmagasinées selon la norme SF dans un tableau de données, une structure comportant deux dimensions (avec lignes et colonnes).

Dans le bloc suivant, nous allons importer un fichier avec une extension .shp (shapefile). Il a été récupéré sur le site de Données Québec et contient les données géographiques des quartiers sociologiques de Montréal. Nous allons supprimer certaines colonnes sans intérêt immédiat, puis y ajouter une nouvelle colonne appelée regex (pour “expression régulière”). Ces expressions régulières permettront ensuite d’associer les noms de quartiers trouvés dans les textes du corpus à des noms de quartier standardisés.

# Importation d'une table comprenant les noms de quartiers et les coordonnées correspondant à leurs délimitations
quartiersGeoMtl <-
  sf::read_sf(
    "donnees/donnees_importees/quartiers_sociologiques_2014/quartiers_sociologiques_2014.shp"
  )

# print(quartiersGeoMtl)

# str(quartiersGeoMtl)

# Sélection des colonnes d'intérêt
quartiersGeoMtl <- quartiersGeoMtl[, c("Q_socio", "Abrev", "geometry")]

# Observation de cette structure de données
str(quartiersGeoMtl)
sf [32 × 3] (S3: sf/tbl_df/tbl/data.frame)
 $ Q_socio : chr [1:32] "Ahuntsic" "Bordeaux-Cartierville" "Anjou" "Côte-des-Neiges" ...
 $ Abrev   : chr [1:32] "AHU" "AHU" "AJ" "CDN" ...
 $ geometry:sfc_MULTIPOLYGON of length 32; first list element: List of 1
  ..$ :List of 1
  .. ..$ : num [1:564, 1:2] 289315 289329 289366 289385 289420 ...
  ..- attr(*, "class")= chr [1:3] "XY" "MULTIPOLYGON" "sfg"
 - attr(*, "sf_column")= chr "geometry"
 - attr(*, "agr")= Factor w/ 3 levels "constant","aggregate",..: NA NA
  ..- attr(*, "names")= chr [1:2] "Q_socio" "Abrev"
# Ajout d'une colonne avec 32 expressions régulières, une pour chaque nom de quartier.
quartiersGeoMtl$regex <- c(
  "[Aa]huntsic",
  "[Bb]ordeaux.[Cc]artierville",
  "[Aa]njou",
  "([Cc][ôo]te.des.[Nn]eiges)|(cdn|CDN)",
  "([Nn]otre.[Dd]ame.de[Gg]r[aâ]ce)|(NDG|ndg\b)",
  "[Nn]ord.[Oo]uest.{1,14}[Mm]ontréal",
  "[Ll]achine",
  "[Ll]a[Ss]alle",
  "[Hh]ochelag",
  "[Mm]ercier.[Oo]uest",
  "[Mm]ercier.[Ee]uest",
  "[Mm]ontréal.[Nn]ord",
  "[Oo]tremont",
  "([Pp]lateau.[Mm]ont.[Rr]oyal)|le Plateau",
  "[Pp]ointe.aux.[Tt]rembles",
  "[Rr]ivière.des.[Pp]rairies",
  "[Pp]etite.[Pp]atrie",
  "[Rr]osemont",
  "[Vv]ille.[Ss]aint.[Ll]aurent",
  "[Ss]aint.[Ll]éonard",
  "[Pp]etite.[Bb]ourgogne",
  "([Pp]ointe.[Ss]aint.[Cc]arles)|(\bpsc\b|PSC)",
  "[Ss]aint.[Hh]enri",
  "([Vv]ille.[EÉée]mard|[Cc][ôo]te.[Ss]aint.[Pp]aul)",
  "[Vv]erdun",
  "[Cc]entre.[Ss]ud",
  "[Ff]aubourg.[Ss]aint.[Ll]aurent",
  "([Pp]eter|[Qq]artier).[Mm]c.?[Gg]ill",
  "[Vv]ieux.[Mm]ontréal",
  "[Pp]arc.[Ee]xtension",
  "[Ss]aint.[Mm]ichel",
  "[Vv]illeray"
)

# On peut faire un test d'indexation avec avec Côte-des-Neiges (4e expression régulière)
# xyz[texte %like% quartiersGeoMtl$regex[4]]

Extraction des noms de quartiers dans les textes

Les expressions régulières permettront de repérer sinon la totalité, du moins le plus grand nombre de mention des noms de quartiers dans les textes du corpus. Ces expressions seront d’abord transformées en un seul long vecteur, chaque expression étant séparée des autres par l’opérateur | (équivalent de “ou”). Le vecteur sera ensuite projeté dans les textes du corpus et attrapera, pour ainsi dire, les noms de quartiers. Chaque fois qu’un texte comprendra une chaîne de caractères répondant aux critères de l’expression, cette chaine sera extraite du texte et placée dans une nouvelle colonne appelée quartiers_nommes.

# Première étape: créer une seule et longue regex avec celles de la colonne du même nom
regex_tous_quartiers <- paste(quartiersGeoMtl$regex, sep = "", collapse = "|")

# Extraction des noms avec cette expression régulière. Les chaînes ainsi extraites seront placées dans une nouvelle colonne appelée "quartiers_nommes".
xyz$quartiers_nommes <- sapply(xyz$texte, str_extract_all, pattern = regex_tous_quartiers)

# str(xyz)

# Élimination des doublons
xyz$quartiers_nommes <- sapply(xyz$quartiers_nommes, unique)

# L'opération d'extraction a généré des listes. 
# On peut assembler les éléments de ces listes avec `paste()`
xyz$quartiers_nommes <- sapply(xyz$quartiers_nommes, paste, collapse = "; ")

# Vérification du succès de l'opération
xyz[quartiers_nommes != "", .(quartiers_nommes)]
                              quartiers_nommes
 1:                             Vieux-Montréal
 2:                                 Centre-Sud
 3:                                   Hochelag
 4:                                   Hochelag
 5: Ahuntsic; Hochelag; Saint-Michel; Rosemont
 6:                                   Ahuntsic
 7:                       rivière des Prairies
 8:                                   Rosemont
 9:                                   Hochelag
10:                                 Centre-Sud
11:                        Pointe-aux-Trembles
12:                            Côte-des-Neiges
13:                                   Hochelag
14:                                 le Plateau
15:                             Vieux-Montréal
16:                                     Verdun
17:                                   Rosemont
18:                       rivière des Prairies
19:                                Saint-Henri
20:                                     verdun
21:          Villeray; petite patrie; Rosemont
22:                                Saint-Henri
23:                                Ville-Émard
24:                                 le Plateau
25:                                Saint-Henri
26:                                     Verdun
27:                                      Anjou
28:                                   Rosemont
29:                              Montréal-Nord
30:                  Montréal-Nord; Centre-Sud
31:                                 le Plateau
                              quartiers_nommes
# Conversion des chaines extraites des textes en noms officiels 
xyz$quartiers_nommes <- stri_replace_all_regex(
    xyz$quartiers_nommes,
    pattern = quartiersGeoMtl$regex,
    replacement = quartiersGeoMtl$Q_socio,
    vectorize = FALSE
  )

Constitution d’une table pour visualisation

Nous avons maintenant deux tables à notre disposition. La première contient les textes et les métadonnées, ainsi qu’une colonne où sont indiqués les quartiers nommés dans les textes. La deuxième est un un objet de type SF et contient ces mêmes noms de quartiers associés à des coordonnées géographiques. Pour passer à la prochaine étape, soit la projection de la fréquence des mentions de quartiers dans une carte géographique, nous devons créer une troisième table qui contiendra trois informations: le nom du quartier, le nombre de fois que ce quartier est mentionné dans l’ensemble du corpus, et les données géographiques de ces quartiers.

# Création d'une liste comprenant tous les noms de quartiers
quartiers_nommes_sep_l <- strsplit(xyz$quartiers_nommes, "; ")

# Élimination des éléments sans contenu
valeurs_non_nulles <- which(quartiers_nommes_sep_l != "character(0)")
quartiers_nommes_sep_l <- quartiers_nommes_sep_l[valeurs_non_nulles]

# Extraction des noms emmagasinés dans la liste et transfert de ces noms dans un tableau de données
quartiers_empiles <- data.table(
  Q_socio = c(
    sapply(quartiers_nommes_sep_l, "[", 1),
    sapply(quartiers_nommes_sep_l, "[", 2),
    sapply(quartiers_nommes_sep_l, "[", 3),
    sapply(quartiers_nommes_sep_l, "[", 4)
  )
)

# Calcul de la fréquence des noms de quartiers
freq_quartiers <- quartiers_empiles[!is.na(Q_socio) , .N, "Q_socio"]

# En base R, la même opération serait faite comme suit:
# quartiers_empiles_sansNA <- quartiers_empiles[!is.na(quartiers_empiles$Q_socio),]
# freq_quartiers_df <- data.frame(Q_socio = names(table(quartiers_empiles_sansNA)),
#                                N = unname(as.integer(table(quartiers_empiles_sansNA))))

# On joint maintenant la table de fréquence et celle contenant les coordonnées géographiques
freq_quartiers_geo <- left_join(
  quartiersGeoMtl[, c("Q_socio", "Abrev", "geometry")],
  freq_quartiers, by ="Q_socio"
  )

# Cette opération a introduit dans la table de fréquence des noms de quartiers qui n'y étaient pas et dont la fréquence est NA. Nous allons transformer ces NA en 0.
freq_quartiers_geo$N <- ifelse(
  is.na(freq_quartiers_geo$N),
  0,
  freq_quartiers_geo$N
  )

Les graphiques

Comme n’importe quel jeu de données, celui que nous avons créé peut être observé sous la forme d’un diagramme de dispersion ou à points.

ggplot(freq_quartiers_geo, aes(x=reorder(Q_socio, N), y=N, colour = N, size = N))+
  geom_jitter(stat = "identity")+
  coord_flip()+
  theme_classic()

Un tel diagramme ne tire cependant pas parti des données géographiques du tableau. L’extension ggplot2 qu’on vient d’utiliser pour créer le diagramme de dispersion permet d’ajouter une couche graphique. La fonction prend en entrée l’objet sf et repère automatiquement la colonne geo contenant les coordonnées de polygones.

ggplot()+
  geom_sf(data=freq_quartiers_geo, aes(fill=-N))

Dans la suite de l’atelier, on utilisera plutôt des extensions spécialisées.

Projection des dimensions dans l’espace géographique

Plusieurs extensions permettent d’utiliser les données géographiques pour produire des cartes statiques et interactives. Leaflet et tmap sont parmi les plus utilisées. D’autres, comme mapview, s’appuient sur leaflet et simplifient le processus. Vous trouverez en ligne une documentation abondante et plusieurs exemples avec divers jeux de données

L’extension tmap

L’extension tmap fonctionne sur le principe de la grammaire des graphiques dont il a été question dans le précédent atelier sur ggplot2. On fournit à la fonction d’entrée tm_shape() un jeu de données et on ajoute ensuite des couches pour ajuster les formes et autres éléments tels les titres, symboles, légendes, etc.

tmap_mode("plot")
tmap mode set to plotting
# Fonction de base, à laquelle on ajoute les polygones
tm_shape(freq_quartiers_geo) + tm_fill(col = "N",  palette = "viridis") +
  tm_layout(legend.height = 0.5)

# Avec des tranches
tm_shape(freq_quartiers_geo) + tm_polygons(col = "N", breaks = c(0,3,5), palette = "viridis") +
  tm_layout(legend.height = 0.5)

# Avec algorithme (jenks = identification des groupes similaires)
tm_shape(freq_quartiers_geo) + tm_polygons(col = "N", style = "jenks", palette = "viridis") +
  tm_layout(legend.height = 0.5)

# Avec une seule couleur par catégorie
tm_shape(freq_quartiers_geo) + tm_polygons(col = "N", style = "cat", palette = "viridis") +
  tm_layout(legend.height = 0.5)

# Ajout de composantes graphiques (boussole, titres, etc.)
freq_shape <- tm_shape(freq_quartiers_geo) + tm_polygons(col = "N", style = "cat", palette = "viridis") +
  tm_layout(legend.height = 0.5)

# Ajout d'une boussole
freq_shape + tm_compass(type = "8star", position = c("left", "top"))

# Ajout d'un titre
freq_shape + tm_layout(title = "Quartiers montréalais dans les nouvelles\nde la revue XYZ")

# Taille des éléments
freq_shape + tm_layout(title = "Quartiers montréalais dans les nouvelles\nde la revue XYZ", title.size = 0.8)

# Couleur de l'arrière-plan
freq_shape + tm_layout(title = "Quartiers montréalais dans les nouvelles\nde la revue XYZ", title.size = 0.8, bg.color = "grey85")

# Élimination du cadre
freq_shape + tm_layout(title = "Quartiers montréalais dans les nouvelles\nde la revue XYZ", title.size = 0.8, frame = FALSE)

# Ajout d'un titre et des crédits, puis déplacement de la légende
freq_shape + tm_layout(title = "Quartiers montréalais dans les nouvelles\nde la revue XYZ", scale = 0.7, frame = FALSE) +
  tm_credits("Source des données: XYZ", position = c("right", "BOTTOM"))

# Utilisation d'une feuille de styles
tm_shape(freq_quartiers_geo) + tm_polygons(col = "N") + tm_style("bw")

tm_shape(freq_quartiers_geo) + tm_polygons(col = "N") + tm_style("classic")

tm_shape(freq_quartiers_geo) + tm_polygons(col = "N") + tm_style("cobalt")

tm_shape(freq_quartiers_geo) + tm_polygons(col = "N") + tm_style("col_blind")

# Ajout des abréviations de quartiers (ici, on aurait pu conserver et utiliser les codes de quartiers pour éviter les superpositions)
tm_shape(freq_quartiers_geo) + tm_polygons(col = "N") + 
  tm_style("col_blind") +
  tm_text("Abrev", size = 0.4)

Deuxième tâche

Nous allons maintenant composer un vecteur comprenant quatre expressions régulières. Elles seront utilisées pour extraire des textes les noms des universités montréalaises.

# Expressions régulières unies en une seule chaîne de caractères
regex_univ <- c("université.de.montréal|\\bu\\.?de?\\.?m\\.?\\b|université.du.québec.à.montréal|\\buq[aà]m\\b|concordia|mcgill")

# Extraction des noms
xyz[, univ_nommees:=str_extract_all(tolower(texte), regex_univ)]

# Remplacement des character(0) par NA dans la nouvelle colonne du tableau
xyz$univ_nommees <- lapply(xyz$univ_nommees, function(x) if(identical(x, character(0))) "" else x)

# Élimination des doublons
xyz[, univ_nommees:=lapply(univ_nommees, unique)]

# L'opération d'extraction a généré des listes. 
# On peut assembler les éléments de ces listes avec `paste()`
xyz$univ_nommees <- sapply(xyz$univ_nommees, function(x) paste(x, collapse = "; "))

# Vérification de l'opération
xyz[univ_nommees != "", .(univ_nommees)]
                    univ_nommees
 1:       université de montréal
 2:       université de montréal
 3:                         uqam
 4:       université de montréal
 5:              uqam; concordia
 6:                       mcgill
 7: uqam; université de montréal
 8:       université de montréal
 9:                       mcgill
10:       université de montréal
# Les noms étant uniformes et "propres", nous n'aurons pas besoin de les normaliser comme on l'a fait pour les noms de quartiers.

Création d’une table de fréquence avec les entités géographiques

# Création d'une liste comprenant tous les noms de quartiers
univ_nommees_sep_l <- strsplit(xyz$univ_nommees, "; ")

# Élimination des éléments sans contenu
valeurs_non_nulles <- which(univ_nommees_sep_l != "character(0)")
univ_nommees_sep_l <- univ_nommees_sep_l[valeurs_non_nulles]

# On vérifie le nombre maximal de valeurs uniques dans chaque texte
max(sapply(univ_nommees_sep_l, length))
[1] 2
# Transposition des données dans un tableau
univ_empilees <- data.table(
  nom_univ = c(
    sapply(univ_nommees_sep_l, "[", 1),
    sapply(univ_nommees_sep_l, "[", 2)
  )
)

# Table de fréquences
freq_univ <- univ_empilees[
  !is.na(nom_univ) , .N, "nom_univ"][
    order(N, decreasing = TRUE)
    ]

# Pour faciliter le géoréférencement, nous allons ajouter une colonne avec l'adresse de chaque institution

freq_univ$adresse <- c("3150, rue Jean-Brillant, Montreal, Quebec, Canada",
                       "1430, rue Saint-Denis, Montreal, Quebec, Canada",
                       "845, rue Sherbrooke Ouest, Montreal, Quebec, Canada",
                       "1455, boulevard de Maisonneuve Ouest, Montreal, Quebec, Canada")

Travailler avec Tidygeocoder

Nous sommes maintenant prêts à géoréférencer nos données. Les adresses seront passées à la fonction geocode() de l’extension tidygeocoder. Celle-ci traduit nos requêtes, écrites en langage R, dans un format que l’API puisse interpréter (normalement json). La réponse de l’API, une fois reçue par tidygeocoder, est transformée en un tableau de données de type tibble. Si vos adresses ne sont pas déjà intégrées à un tableau de données, vous pouvez les passer sous la forme d’un vecteur à la fonction tidygeocoder::geo(). Essayez par exemple avec une adresse que vous connaissez bien.

# Exercice: remplacer l'adresse dans l'instruction ci-dessous par une adresse de votre choix, puis observez le résultat.
mon_adresse <- geo(address = "1000, rue de la Gauchetière, Montréal, Québec, Canada")
Passing 1 address to the Nominatim single address geocoder
Query completed in: 1 seconds

Comme nos adresses sont dans un tableau de données, nous allons utiliser la fonction geocode(). Nous allons ensuite transformer le tableau de données en un objet de type Simple Feature.

# Géoréférencement des universités montréalaises
freq_univ_geo <- freq_univ |> tidygeocoder::geocode(address = adresse, method = "osm")
Passing 4 addresses to the Nominatim single address geocoder
Query completed in: 4 seconds
# Transformation du tableau de données en objet Simple Feature
freq_univ_geo_sf <- st_as_sf(freq_univ_geo, coords = c("long", "lat"), crs = st_crs(4326))

# Création d'une colonne avec des abréviations
freq_univ_geo_sf$Abrev <- c("UdeM", "UQAM", "McGill", "Concordia")
# Si le géoréférencement échoue, vous pouvez exécuter l'instruction ci-dessous
# freq_univ_geo_sf <- readRDS("donnees/donnees_produites/rds/freq_univ_geo_sf.RDS")

Les coordonnées du polygone de l’île de Montréal pourront nous être utilisées pour créer une couche sur laquelle les points seront déposés.

# Importation des coordonnées à partir du site de la Ville de Montréal
mtl_poly <- geojson_sf("https://data.montreal.ca/dataset/b628f1da-9dc3-4bb1-9875-1470f891afb1/resource/92cb062a-11be-4222-9ea5-867e7e64c5ff/download/limites-terrestres.geojson")

# Observation rapide de la forme
mtl_poly |> mapview()

Nous sommes maintenant prêts à projeter les points sur un plan géographique. Nous utiliserons la fonction tm_bubbles(), qui permet d’adapter le volume des points à une variable numérique (notre variable N):

tmap_mode("plot")
tmap mode set to plotting
tm_shape(mtl_poly) + tm_polygons() +
tm_shape(freq_univ_geo_sf) + tm_bubbles(size = "N", col = "red")

tmap_mode("view")
tmap mode set to interactive viewing
tm_shape(mtl_poly) + tm_polygons() +
tm_shape(freq_univ_geo_sf) + tm_bubbles(size = "N", col = "red")
Legend for symbol sizes not available in view mode.
# Le mode "view" permet d'utiliser des couches de base variées:
tmap_mode("view")
tmap mode set to interactive viewing
# Modification de paramètres
tm_basemap(server = "Stamen.Toner") + 
tm_shape(mtl_poly) + tm_polygons(col="white", border.col = "blue") +
tm_shape(freq_univ_geo_sf) + tm_bubbles(size = "N", col = "red", scale = 3)
Legend for symbol sizes not available in view mode.
# Le mode "plot" permet de d'ajouter d'autres éléments visuels
tmap_mode("plot")
tmap mode set to plotting
tm_shape(mtl_poly) + tm_polygons(col="white", border.col = "blue") +
tm_shape(freq_univ_geo_sf) + tm_bubbles(size = "N", col = "red", scale = 3)+
tm_text(text = "Abrev", size = 0.5)+
tm_credits("Source des données: Revue XYZ") + 
tm_layout(main.title = "Universités montréalaises dans\nles nouvelles de la revue XYZ",
            fontface = "bold",
          title.size = 0.5)

# Fonction de sauvegarde
mtl_univ_plot <- tm_shape(mtl_poly) + tm_polygons(col="white", border.col = "blue") +
tm_shape(freq_univ_geo_sf) + tm_bubbles(size = "N", col = "red", scale = 3)+
tm_text(text = "nom_univ", size = 0.5)+
tm_credits("Source des données: Revue XYZ") + 
tm_layout(main.title = "Universités montréalaises dans\nles nouvelles de la revue XYZ",
            fontface = "bold",
          title.size = 0.5)

tmap_save(
  tm = mtl_univ_plot,
  filename = "donnees/donnees_produites/graphiques/mtl_univ_plot.png"
)
Map saved to /Users/pascalbrissette/github/PERSONNEL/Ateliers/20230406_PB_atelier_ladirec_geo/donnees/donnees_produites/graphiques/mtl_univ_plot.png
Resolution: 2263.864 by 1947.997 pixels
Size: 7.546214 by 6.493322 inches (300 dpi)
# Exercice:
# Modifiez l'échelle et la couleur des bulles
# Modifiez les modes de visualisation ("plot", "view")
# La fonction `tm_basemap()` permet d'importer des toiles de fond variées. Remplacez "Stamen.Toner" par "Stamen.Watercolor" ou "Esri.WorldImagery". D'autres options sont proposées https://leaflet-extras.github.io/leaflet-providers/preview/

Pour aller plus loin

Jesse Cambon, Diego Hernangomez, Christopher Belanger, Daniel Possenriede, “Tidygeocoder: An R package for geocoding”, 2021. DOI: https://doi.org/10.21105/joss.03544

Lovelace, Robin, Jakub Nowosad et Jannes Muenchow, Geocomputation With R, 2020. URL: https://r.geocompx.org/index.html

Martijn Tennekes et Jakub Nowosad, Elegant and Informative Maps With Tmap, 2021. URL: https://r-tmap.github.io/tmap-book/